﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Text;
using System.Threading;

using Utilities.IO;

namespace Utilities.Net
{
    public abstract class Connection : IDisposable
    {
        public TcpClient client;
        public Stream stream;
        public BinaryReader reader;
        public BinaryWriter writer;

        public BitOrder bitOrder;
        public Type stringPrefixType = typeof(byte);

        public virtual bool Connected
        {
            get
            {
                return client != null && client.Connected;
            }
        }
        public virtual string HostName
        {
            get
            {
                if (Address.Equals(IPAddress.Loopback))
                    return "localhost";
                try
                {
                    IAsyncResult asyncResult = Dns.BeginGetHostEntry(Address, null, null);
                    if (asyncResult.AsyncWaitHandle.WaitOne(1000, false))
                        return Dns.EndGetHostEntry(asyncResult).HostName;
                    else
                        return Address.ToString();
                }
                catch
                {
                    return Address.ToString();
                }
            }
        }
        public virtual IPAddress Address
        {
            get
            {
                if (Connected)
                    return (client.Client.RemoteEndPoint as IPEndPoint).Address;
                else
                    return IPAddress.None;
            }
        }
        public virtual int Port
        {
            get
            {
                if (Connected)
                    return (client.Client.RemoteEndPoint as IPEndPoint).Port;
                else
                    return -1;
            }
        }

        private bool debugging = false;
        public bool Debugging
        {
            get
            {
                return debugging;
            }
            set
            {
                debugging = value;

                if (debugging)
                {
                    if (DebugStream != null)
                        DebugStream.Close();
                    DebugStream = new MemoryStream();
                    debugStreamWriter = new BinaryWriter(DebugStream);
                }
            }
        }
        public MemoryStream DebugStream { get; set; }
        private BinaryWriter debugStreamWriter;

        public Connection() : this(BitOrder.LittleEndian, typeof(sbyte))
        {
        }
        public Connection(BitOrder bitOrder) : this(bitOrder, typeof(sbyte))
        {
        }
        public Connection(BitOrder bitOrder, Type stringPrefixType)
        {
            this.bitOrder = bitOrder;
            this.stringPrefixType = stringPrefixType;
        }
        public Connection(Connection connection)
        {
            client = connection.client;
            stream = connection.stream;
            reader = connection.reader;
            writer = connection.writer;
            bitOrder = connection.bitOrder;
            stringPrefixType = connection.stringPrefixType;
        }

        #region Fonctions de lecture

        public virtual bool ReadBool()
        {
            return reader.ReadByte() != 0;
        }
        public virtual sbyte ReadSByte()
        {
            return reader.ReadSByte();
        }
        public virtual byte ReadByte()
        {
            return reader.ReadByte();
        }
        public virtual byte[] ReadBytes(int count)
        {
            return reader.ReadBytes(count);
        }
        public virtual int Read(byte[] buffer, int offset, int count)
        {
            return stream.Read(buffer, offset, count);
        }
        public virtual short ReadInt16()
        {
            if (bitOrder == BitOrder.LittleEndian)
                return reader.ReadInt16();
            else
            {
                byte[] data = ReadBytes(sizeof(short));
                Array.Reverse(data);
                return BitConverter.ToInt16(data, 0);
            }
        }
        public virtual ushort ReadUInt16()
        {
            if (bitOrder == BitOrder.LittleEndian)
                return reader.ReadUInt16();
            else
            {
                byte[] data = ReadBytes(sizeof(ushort));
                Array.Reverse(data);
                return BitConverter.ToUInt16(data, 0);
            }
        }
        public virtual int ReadInt32()
        {
            if (bitOrder == BitOrder.LittleEndian)
                return reader.ReadInt32();
            else
            {
                byte[] data = ReadBytes(sizeof(int));
                Array.Reverse(data);
                return BitConverter.ToInt32(data, 0);
            }
        }
        public virtual uint ReadUInt32()
        {
            if (bitOrder == BitOrder.LittleEndian)
                return reader.ReadUInt32();
            else
            {
                byte[] data = ReadBytes(sizeof(uint));
                Array.Reverse(data);
                return BitConverter.ToUInt32(data, 0);
            }
        }
        public virtual long ReadInt64()
        {
            if (bitOrder == BitOrder.LittleEndian)
                return reader.ReadInt64();
            else
            {
                byte[] data = ReadBytes(sizeof(long));
                Array.Reverse(data);
                return BitConverter.ToInt64(data, 0);
            }
        }
        public virtual ulong ReadUInt64()
        {
            if (bitOrder == BitOrder.LittleEndian)
                return reader.ReadUInt64();
            else
            {
                byte[] data = ReadBytes(sizeof(ulong));
                Array.Reverse(data);
                return BitConverter.ToUInt64(data, 0);
            }

        }
        public virtual float ReadSingle()
        {
            if (bitOrder == BitOrder.LittleEndian)
                return reader.ReadSingle();
            else
            {
                byte[] data = ReadBytes(sizeof(float));
                Array.Reverse(data);
                return BitConverter.ToSingle(data, 0);
            }
        }
        public virtual double ReadDouble()
        {
            if (bitOrder == BitOrder.LittleEndian)
                return reader.ReadDouble();
            else
            {
                byte[] data = ReadBytes(sizeof(double));
                Array.Reverse(data);
                return BitConverter.ToDouble(data, 0);
            }
        }
        public virtual string ReadString()
        {
            if (stringPrefixType == typeof(sbyte))
                return reader.ReadString();
            else
            {
                int length = 0;
                if (stringPrefixType == typeof(byte)) length = ReadSByte();
                if (stringPrefixType == typeof(short)) length = ReadInt16();
                if (stringPrefixType == typeof(ushort)) length = ReadUInt16();
                if (stringPrefixType == typeof(int)) length = ReadInt32();
                byte[] data = ReadBytes(length);
                return Encoding.Default.GetString(data);
            }
        }
        public virtual T Read<T>()
        {
            return (T)Read(typeof(T));
        }
        public virtual object Read(Type type)
        {
            // Enumérations
            if (type.IsEnum)
                type = Enum.GetUnderlyingType(type);

            // Types basiques
            if (type == typeof(sbyte)) return ReadSByte();
            if (type == typeof(byte)) return ReadByte();
            if (type == typeof(short)) return ReadInt16();
            if (type == typeof(ushort)) return ReadUInt16();
            if (type == typeof(int)) return ReadInt32();
            if (type == typeof(uint)) return ReadUInt32();
            if (type == typeof(long)) return ReadInt64();
            if (type == typeof(ulong)) return ReadUInt16();
            if (type == typeof(float)) return ReadSingle();
            if (type == typeof(double)) return ReadDouble();
            if (type == typeof(string)) return ReadString();

            // Tableaux
            if (type.IsArray)
            {
                type = type.GetElementType();
                int length = ReadInt32();

                Array array = Array.CreateInstance(type, length);
                for (int i = 0; i < length; i++)
                    array.SetValue(Read(type), i);

                return array;
            }

            // Structures et classes
            if (type.IsValueType || type.GetCustomAttributes(true).Count(o => o is SerializableAttribute) > 0)
            {
                IEnumerable<FieldInfo> fields = type.GetFields().Where(f => f.GetCustomAttributes(true).Count(o => o is NonSerializedAttribute) == 0);
                object obj = Activator.CreateInstance(type);
                foreach (FieldInfo field in fields)
                    field.SetValue(obj, Read(field.FieldType));

                return obj;
           }

            // Elément non supporté
            throw new NotSupportedException();
        }

        public virtual void ReadAsync<T>(Action<T> method)
        {
            buffer = new byte[SizeOf<T>()];
            this.method = method;
            stream.BeginRead(buffer, 0, SizeOf<T>(), new AsyncCallback(OnReadAsync<T>), null);
        }
        public virtual void ReadAsync(Action method)
        {
            new Thread(() =>
                {
                    while (client.Available == 0)
                        Thread.Sleep(0);
                    method();
                }).Start();
        }

        private byte[] buffer;
        private object method;
        private void OnReadAsync<T>(IAsyncResult asyncResult)
        {
            try
            {
                if (stream.EndRead(asyncResult) != SizeOf<T>())
                    return;
            }
            catch
            {
                for (int i = 0; i < buffer.Length; i++)
                    buffer[i] = 0;
            }
            finally
            {
                if (typeof(T) == typeof(string))
                {
                    int length = 0;
                    if (stringPrefixType == typeof(byte)) length = CastTo<byte>(buffer);
                    if (stringPrefixType == typeof(sbyte)) length = CastTo<sbyte>(buffer);
                    if (stringPrefixType == typeof(short)) length = CastTo<sbyte>(buffer);
                    if (stringPrefixType == typeof(ushort)) length = CastTo<sbyte>(buffer);
                    if (stringPrefixType == typeof(int)) length = CastTo<sbyte>(buffer);
                    buffer = ReadBytes(length);
                }
                ((Action<T>)method)(CastTo<T>(buffer));
            }
        }

        protected int SizeOf<T>()
        {
            return SizeOf(typeof(T));
        }
        protected int SizeOf(Type type)
        {
            if (type.IsEnum)
                return SizeOf(Enum.GetUnderlyingType(type));

            // Types basiques
            if (type == typeof(sbyte)) return sizeof(sbyte);
            if (type == typeof(byte)) return sizeof(byte);
            if (type == typeof(short)) return sizeof(short);
            if (type == typeof(ushort)) return sizeof(ushort);
            if (type == typeof(int)) return sizeof(int);
            if (type == typeof(uint)) return sizeof(uint);
            if (type == typeof(long)) return sizeof(long);
            if (type == typeof(ulong)) return sizeof(ulong);
            if (type == typeof(float)) return sizeof(float);
            if (type == typeof(double)) return sizeof(double);
            if (type == typeof(string)) return SizeOf(stringPrefixType);

            return 0;
        }
        protected T CastTo<T>(byte[] buffer)
        {
            return (T)CastTo(typeof(T), buffer);
        }
        protected object CastTo(Type type, byte[] buffer)
        {
            // Enumération
            if (type.IsEnum)
                return CastTo(Enum.GetUnderlyingType(type), buffer);

            // Type basique
            if (type == typeof(string))
                return Encoding.UTF8.GetString(buffer);
            else
            {
                if (type == typeof(sbyte)) return(sbyte)buffer[0];
                if (type == typeof(byte)) return buffer[0];

                if (bitOrder == BitOrder.BigEndian)
                    Array.Reverse(buffer);

                if (type == typeof(short)) return BitConverter.ToInt16(buffer, 0);
                if (type == typeof(ushort)) return BitConverter.ToUInt16(buffer, 0);
                if (type == typeof(int)) return BitConverter.ToInt32(buffer, 0);
                if (type == typeof(uint)) return BitConverter.ToUInt32(buffer, 0);
                if (type == typeof(long)) return BitConverter.ToInt64(buffer, 0);
                if (type == typeof(ulong)) return BitConverter.ToUInt64(buffer, 0);
                if (type == typeof(float)) return BitConverter.ToSingle(buffer, 0);
                if (type == typeof(double)) return BitConverter.ToDouble(buffer, 0);
            }

            return null;
        }

        #endregion
        #region Fonctions d'écriture

        public virtual void Write(bool value)
        {
            writer.Write((byte)(value ? 1 : 0));
            if (debugging)
                debugStreamWriter.Write((byte)(value ? 1 : 0));
        }
        public virtual void Write(sbyte value)
        {
            writer.Write(value);
            if (debugging)
                debugStreamWriter.Write(value);
        }
        public virtual void Write(byte value)
        {
            writer.Write(value);
            if (debugging)
                debugStreamWriter.Write(value);
        }
        public virtual void Write(byte[] value)
        {
            writer.Write(value);
            if (debugging)
                debugStreamWriter.Write(value);
        }
        public virtual void Write(byte[] buffer, int offset, int count)
        {
            stream.Write(buffer, offset, count);
            if (debugging)
                DebugStream.Write(buffer, offset, count);
        }
        public virtual void Write(short value)
        {
            if (bitOrder == BitOrder.LittleEndian)
            {
                writer.Write(value);
                if (debugging)
                    debugStreamWriter.Write(value);
            }
            else
            {
                byte[] data = BitConverter.GetBytes(value);
                Array.Reverse(data);
                writer.Write(data);
            }
        }
        public virtual void Write(ushort value)
        {
            if (bitOrder == BitOrder.LittleEndian)
            {
                writer.Write(value);
                if (debugging)
                    debugStreamWriter.Write(value);
            }
            else
            {
                byte[] data = BitConverter.GetBytes(value);
                Array.Reverse(data);
                writer.Write(data);
            }
        }
        public virtual void Write(int value)
        {
            if (bitOrder == BitOrder.LittleEndian)
            {
                writer.Write(value);
                if (debugging)
                    debugStreamWriter.Write(value);
            }
            else
            {
                byte[] data = BitConverter.GetBytes(value);
                Array.Reverse(data);
                writer.Write(data);
            }
        }
        public virtual void Write(uint value)
        {
            if (bitOrder == BitOrder.LittleEndian)
            {
                writer.Write(value);
                if (debugging)
                    debugStreamWriter.Write(value);
            }
            else
            {
                byte[] data = BitConverter.GetBytes(value);
                Array.Reverse(data);
                writer.Write(data);
            }
        }
        public virtual void Write(long value)
        {
            if (bitOrder == BitOrder.LittleEndian)
            {
                writer.Write(value);
                if (debugging)
                    debugStreamWriter.Write(value);
            }
            else
            {
                byte[] data = BitConverter.GetBytes(value);
                Array.Reverse(data);
                writer.Write(data);
            }
        }
        public virtual void Write(ulong value)
        {
            if (bitOrder == BitOrder.LittleEndian)
            {
                writer.Write(value);
                if (debugging)
                    debugStreamWriter.Write(value);
            }
            else
            {
                byte[] data = BitConverter.GetBytes(value);
                Array.Reverse(data);
                writer.Write(data);
            }
        }
        public virtual void Write(float value)
        {
            if (bitOrder == BitOrder.LittleEndian)
            {
                writer.Write(value);
                if (debugging)
                    debugStreamWriter.Write(value);
            }
            else
            {
                byte[] data = BitConverter.GetBytes(value);
                Array.Reverse(data);
                writer.Write(data);
            }
        }
        public virtual void Write(double value)
        {
            if (bitOrder == BitOrder.LittleEndian)
            {
                writer.Write(value);
                if (debugging)
                    debugStreamWriter.Write(value);
            }
            else
            {
                byte[] data = BitConverter.GetBytes(value);
                Array.Reverse(data);
                writer.Write(data);
            }
        }
        public virtual void Write(string value)
        {
            if (stringPrefixType == typeof(sbyte))
            {
                writer.Write(value);
                if (debugging)
                    debugStreamWriter.Write(value);
            }
            else
            {
                if (stringPrefixType == typeof(byte)) Write((byte)value.Length);
                if (stringPrefixType == typeof(short)) Write((short)value.Length);
                if (stringPrefixType == typeof(ushort)) Write((ushort)value.Length);
                if (stringPrefixType == typeof(int)) Write(value.Length);
                writer.Write(Encoding.Default.GetBytes(value));
            }
        }
        public virtual void Write(Array value)
        {
            int length = value.GetLength(0);
            Write(length);

            for (int i = 0; i < length; i++)
                Write(value.GetValue(i));
        }
        public virtual void Write(object value)
        {
            Type type = value.GetType();

            // Enumérations
            if (type.IsEnum)
                type = Enum.GetUnderlyingType(type);

            // Types basiques
            if (type == typeof(sbyte)) Write((sbyte)value);
            else if (type == typeof(byte)) Write((byte)value);
            else if (type == typeof(short)) Write((short)value);
            else if (type == typeof(ushort)) Write((ushort)value);
            else if (type == typeof(int)) Write((int)value);
            else if (type == typeof(uint)) Write((uint)value);
            else if (type == typeof(long)) Write((long)value);
            else if (type == typeof(ulong)) Write((ulong)value);
            else if (type == typeof(float)) Write((float)value);
            else if (type == typeof(double)) Write((double)value);
            else if (type == typeof(string)) Write((string)value);

            // Tableaux
            else if (type.IsArray)
                Write((Array)value);

            // Structures et classes
            else if (type.IsValueType || type.GetCustomAttributes(true).Count(o => o is SerializableAttribute) > 0)
            {
                IEnumerable<FieldInfo> fields = type.GetFields().Where(f => f.GetCustomAttributes(true).Count(o => o is NonSerializedAttribute) == 0);
                foreach (FieldInfo field in fields)
                    Write(field.GetValue(value));
            }

            // Elément non supporté
            else
                throw new NotSupportedException();
        }

        #endregion

        public virtual void Disconnect()
        {
            client.Close();
        }
        public virtual void Dispose()
        {
            Disconnect();
        }
    }
}
